package org.fizz.ass;

import java.io.IOException;
import java.io.Reader;

/**
 * 解析HTML文档<br>
 * 解析说明：<br>
 * 1.DOCTYPE: &lt;!DOCTYPE html&gt;<br>
 * 2.注释: &lt;!-- ... --&gt;。不管&lt;!-- ... --&gt;之间有什么字符，...都属于注释的标签内字符<br>
 * 3.Script标签: &lt;script &gt; ... &lt;/script&gt;。不管&lt;script &gt; ... &lt;/script&gt;之间有什么字符，...都属于Script标签外字符<br>
 * 4.Style标签: 类似Script标签<br>
 * 5.普通标签: &lt;&gt; ... &lt;/&gt;<br>
 * 6.简单标签: &lt; ... /&gt;
 * @author FIZZ
 * @version ass-1.0, 2014-4-21
 * @since 1.0
 */
public class AssDocumentParser implements AssConstants{

	/** 字符流 */
	private Reader reader;
	/** 解析HTML的回调类 */
	private IAssParserCallback parserCallback;
	/** 一次仅仅读入一个字符的缓存 */
	private char[] currentChar = new char[1];//一次仅仅读入一个字符放进缓存
	/** 当前的读取位置 */
	private int currentPosition = 0;
	
	/** 普通标签是否开始 */
	private boolean isTagStart = false;
	
	/** Script标签是否开始 */
	private boolean isScriptTagStart = false;
	
	/** Style标签是否开始 */
	private boolean isStyleTagStart = false;
	
	/** 注释标签是否开始 */
	private boolean isCommentStart = false;
	
	/** 标签内的内容 */
	private StringBuilder tagContent = new StringBuilder();
	/** 标签外的内容 */
	private StringBuilder text = new StringBuilder();
	
	public AssDocumentParser(Reader reader, IAssParserCallback parserCallBack){
		this.reader = reader;
		this.parserCallback = parserCallBack;
		
		parse();
		
//		this.parserCallback.handlerReadEnding();
		readEndingHandler();
	}
	
	/**
	 * 读取并返回下一个字符
	 * @return 下一个字符
	 * @since 1.0
	 */
	public int readChar(){
		try {
			int k = reader.read(currentChar);
			if(k == -1){
				return -1;
			}
			currentPosition++;
			return currentChar[0];
		} catch (IOException e) {
			e.printStackTrace();
			throw new RuntimeException("bad reader: " + e);
		}
	}
	
	/**
	 * 读取并返回接下来的n个字符串
	 * @param n n个
	 * @return n个字符串
	 * @since 1.0
	 */
	private String readNumChar(int n){
		try {
			char[] nChars = new char[n];
			int k = reader.read(nChars);
			if(k == -1){
				return "";
			}
			currentPosition += n;
			return new String(nChars);
		} catch (IOException e) {
			e.printStackTrace();
			throw new RuntimeException("bad reader: " + e);
		}
	}
	
	/**
	 * 循环读取每个字符并调用处理方法
	 * @since 1.0
	 */
	public void parse(){
		int charInt = 0;
		while(charInt != -1){
			charInt = readChar();
			char ch = (char)charInt;
			charHandler(ch);
		}
	}
	
	/**
	 * 处理单个字符
	 * @param ch 字符
	 * @since 1.0
	 */
	public void charHandler(char ch){
		switch(ch){
			case LT:
				ltHandler(ch);
				break;
			case GT:
				gtHandler(ch);
				break;
			default:
				defaultCharHandler(ch);
				break;
		}
	}
	
	/**
	 * 遇到左间括号'&lt;'时的处理
	 * @param ch &lt;字符
	 * @since 1.0
	 */
	public void ltHandler(char ch){
		if(isScriptTagStart){//是否是Script标签的开始
			if(isAcrossScriptTagEnd(ch)){//是否即将遇到Script的结束标签
				scriptTagEndHandler();
			}
		}else if(isStyleTagStart){
			if(isAcrossStyleTagEnd(ch)){//是否即将遇到style的结束标签
				styleTagEndHandler();
			}
		}else if(isCommentStart){
			tagContentAppendChar(ch);
		}else{//不满足以上条件，肯定是标签的开始
			isTagStart = true;
			tagContentAppendChar(ch);
		}
	}
	/**
	 * 遇到右间括号'&gt;'时的处理
	 * @param ch &gt;字符
	 * @since 1.0
	 */
	public void gtHandler(char ch){
		if(isScriptTagStart){
			textAppendChar(ch);
		}else if(isStyleTagStart){
			textAppendChar(ch);
		}else if(isCommentStart){
			tagContentAppendChar(ch);
		}else{//不满足以上条件，肯定是标签的结束
			acrossGTHanlder(ch);//遇到>的处理
		}
	}
	
	/**
	 * 默认字符的处理，不是左间括号'&lt;'也不是右间括号'&gt;'的字符
	 * @param ch
	 * @since 1.0
	 */
	public void defaultCharHandler(char ch){
		if(isTagStart){//是否是标签的开始
			tagContentAppendChar(ch);
		}else{
			if(isCommentStart){
				if(ch == SHORT_LINE){
					if(isAcrossCommentEnd(ch)){//是否即将遇到注释的结束标签
						commentEndEndHandler();
					}
				}else{
					tagContentAppendChar(ch);
				}
			}else{
				//if(ch != SPACE && ch != TABS && ch != NEW_LINE && ch != ENTER){
				if(ch != SPACE && ch != TABS && ch != NEW_LINE && ch != ENTER){
					textAppendChar(ch);
				}
			}
		}
	}
	
	/**
	 * 在标签内追加一个字符<br>
	 * e.g. &lt; .... &gt;  或  &lt; ... /&gt;
	 * @param ch 单个字符
	 * @since 1.0
	 */
	public void tagContentAppendChar(char ch){
		tagContentAppendStr(ch+"");
	}
	
	/**
	 * 在标签内追加一个字符串<br>
	 * e.g. &lt; .... &gt;  或  &lt; ... /&gt;
	 * @param str 字符串
	 * @since 1.0
	 */
	public void tagContentAppendStr(String str){
		tagContent.append(str);
	}
	
	/**
	 * 在标签外追加一个字符<br>
	 * e.g. &lt;&gt; ... &lt;/&gt;
	 * @param ch 单个字符
	 * @since 1.0
	 */
	public void textAppendChar(char ch){
		if(ch != SPACE && ch != TABS && ch != NEW_LINE && ch != ENTER){//ch不是空格符、制表符、新行符、回车符
			textAppendStr(ch+"");
		}
	}
	
	/**
	 * 在标签外追加一个字符串<br>
	 * e.g. &lt;&gt; ... &lt;/&gt;
	 * @param str 字符串
	 * @since 1.0
	 */
	public void textAppendStr(String str){
		text.append(str);
	}
	
	/**
	 * 获得并返回标签内的字符串<br>
	 * e.g. &lt; .... &gt;  或  &lt; ... /&gt;。 其中...就是标签内的字符串
	 * @return 标签内的字符串
	 * @since 1.0
	 */
	public String getTagContent(){
		String tagContentStr = tagContent.toString();
//		tagContent.delete(0, tagContent.length());
		return tagContentStr.trim();
	}
	
	/**
	 * 获得并返回标签外的字符串<br>
	 * e.g. &lt;&gt; ... &lt;/&gt;。其中...就是标签外的字符串
	 * @return 标签外的字符串
	 * @since 1.0
	 */
	public String getText(){
		String textStr = text.toString();
//		text.delete(0, text.length());
		return textStr;
	}
	
	/**
	 * 获取当前读取的位置
	 * @return 读取的位置
	 * @since 1.0
	 */
	public int getCurrentPosition(){
		return currentPosition;
	}
	
	/**
	 * 遇到&gt;字符的处理
	 * @param ch &gt;字符
	 * @since 1.0
	 */
	public void acrossGTHanlder(char ch){
		tagContentAppendChar(ch);
		String tag = getTagContent();
		String tagStr = tag.substring(1, tag.length()-1);//去掉前后的间括号
		char startChar = tagStr.charAt(0);//开头的字符
		char endChar = tagStr.charAt(tagStr.length()-1);//结束的字符
		
		if(startChar == FRASL){//说明是个结束标签
//			this.parserCallback.handlerText(getText());
//			this.parserCallback.handlerTagEnd(tag);
			
			tagEndHandler();
			isTagStart = false;
		}else if(endChar == FRASL){//说明是个简单标签
//			this.parserCallback.handlerSimpleTag(tag);
			simpleTagHandler();
			isTagStart = false;
		}else if(startChar == EXCLAMATION_MARK){//说明是Doctype或注释
			if(isDoctype(tagStr)){
//				this.parserCallback.handlerDoctype(tagStr);
				doctypHandler();
			}else{
				char rec1Char = tagStr.charAt(tagStr.length()-1);//倒数第一个字符
				char rec2Char = tagStr.charAt(tagStr.length()-2);//倒数第二个字符
				if(rec1Char == SHORT_LINE && rec2Char == SHORT_LINE){//倒数第一个字符 第二个字符都是短横线，说明注释结束
//					this.parserCallback.handlerComment(tag); 
					commentHandler();
				}else{
					//tagContentAppendStr(tag);
					isCommentStart = true;
					isTagStart = false;//已经不是普通标签的开始了	
				}
			}
			
		}else{//那就是一个标签的开始
			String tagName = getTagName(tagStr);
			if(tagName.equalsIgnoreCase(SCRIPT)){
				isScriptTagStart = true;
				isTagStart = false;//已经不是普通标签的开始了
			}else if(tagName.equalsIgnoreCase(STYLE)){
				isStyleTagStart = true;
				isTagStart = false;//已经不是普通标签的开始了
			}else{//普通标签的开始
				isTagStart = false;
			}
//			this.parserCallback.handlerTagStart(tag);
			tagStartHandler();
		}
	}
	
	/**
	 * 获得标签名
	 * @param tagStr 去掉前后的间括号之后的标签内容
	 * @return 标签名
	 * @since 1.0
	 */
	public String getTagName(String tagStr){
		String[] tagStrs = tagStr.trim().split(BLANK);
		return tagStrs[0];//数组的第一个就是标签名
	}
	
	/**
	 * 是否是DOCTYPE
	 * @param tagStr 去掉前后的间括号之后的标签内容
	 * @return 如果是DOCTYPE返回<tt>true<tt>,否则返回<tt>false<tt>
	 * @since 1.0
	 */
	public boolean isDoctype(String tagStr){
		if(tagStr.length() > DOCTYPE_PREFIX.length() + 2){
			String doctypeStrs = tagStr.trim().substring(1, DOCTYPE_PREFIX.length()+1);
			if(doctypeStrs.equalsIgnoreCase(DOCTYPE_PREFIX)){
				return true;
			}
		}
		return false;//数组的第一个就是标签名
	}
	
	
	/**
	 * 是否即将遇到Script的结束标签
	 * @param ch &lt;字符
	 * @return 如果即将遇到Script的结束标签返回<tt>true<tt>,否则返回<tt>false<tt>
	 * @since 1.0
	 */
	public boolean isAcrossScriptTagEnd(char ch){
		markReader();//标记流读到的当前位置
		
		//如果即将遇到Script的结束标签，scriptTagEnd为</script>
		String scriptTagEnd = ch + readNumChar(SCRIPT.length() + 2);//+2是为了读到 /script>
		char secondChar = scriptTagEnd.charAt(1);//第二个字符应该是 斜线
		String scriptTagName = scriptTagEnd.substring(2, scriptTagEnd.length()-1);
		if(secondChar == FRASL && scriptTagName.equalsIgnoreCase(SCRIPT)){
			tagContentAppendStr(scriptTagEnd);
			return true;
		}
		//不符合条件，scriptTagEnd则是普通字符
		resetReader();//把reader流重新定位到刚刚标记的位置
		textAppendChar(ch);
		//textAppendStr(scriptTagEnd);
		return false;
	}
	
	/**
	 * Script标签结束后的处理
	 * @since 1.0
	 */
	public void scriptTagEndHandler(){
//		if(getText() != null && getText().replaceAll(BLANK, "") != "")
//			this.parserCallback.handlerText(getText());
//		this.parserCallback.handlerTagEnd(getTagContent());
		tagEndHandler();
		
		isScriptTagStart = false;
	}
	
	/**
	 * 是否即将遇到注释的结束标签
	 * @param ch '-'短横线
	 * @return 如果即将遇到注释的结束标签返回<tt>true<tt>,否则返回<tt>false<tt>
	 * @since 1.0
	 */
	public boolean isAcrossCommentEnd(char ch){
		markReader();//标记流读到的当前位置
		
		//如果即将遇到注释的结束标签，commentEnd为 -->
		String commentEnd = ch + readNumChar(2);//+2是为了读到  ->
		char secondChar = commentEnd.charAt(1);//第二个字符应该是  -
		char thirdChar= commentEnd.charAt(2);//第三个字符应该是  >
		
		if(ch == SHORT_LINE && secondChar == SHORT_LINE && thirdChar == GT){
			tagContentAppendStr(commentEnd);
			return true;
		}
		resetReader();//把reader流重新定位到刚刚标记的位置
		return false;
	}
	
	/**
	 * 注释标签结束后的处理
	 * @since 1.0
	 */
	public void commentEndEndHandler(){
//		this.parserCallback.handlerComment(getTagContent());
		commentHandler();
		isCommentStart = false;
	}
	
	/**
	 * 是否即将遇到style的结束标签
	 * @param ch &lt;字符
	 * @return 如果即将遇到style的结束标签返回<tt>true<tt>,否则返回<tt>false<tt>
	 * @since 1.0
	 */
	public boolean isAcrossStyleTagEnd(char ch){
		markReader();//标记流读到的当前位置
		
		//如果即将遇到style的结束标签，styleTagEnd为</style>
		String styleTagEnd = ch + readNumChar(STYLE.length() + 2);//+2是为了读到 /script>
		char secondChar = styleTagEnd.charAt(1);//第二个字符应该是 斜线
		String scriptTagName = styleTagEnd.substring(2, styleTagEnd.length()-1);
		if(secondChar == FRASL && scriptTagName.equalsIgnoreCase(STYLE)){
			tagContentAppendStr(styleTagEnd);
			return true;
		}
		//不符合条件，scriptTagEnd则是普通字符
		resetReader();//把reader流重新定位到刚刚标记的位置
		//textAppendStr(styleTagEnd);
		textAppendChar(ch);
		return false;
	}
	
	/**
	 * Style标签结束后的处理
	 * @since 1.0
	 */
	public void styleTagEndHandler(){
//		this.parserCallback.handlerText(getText());
//		this.parserCallback.handlerTagEnd(getTagContent());
		tagEndHandler();
		
		isStyleTagStart = false;
	}
	
	/**
	 * 清空标签内的内容
	 * @since 1.0
	 */
	public void clearTagContent(){
		tagContent.delete(0, tagContent.length());
	}
	
	/**
	 * 清空标签外的内容
	 * @since 1.0
	 */
	public void clearText(){
		text.delete(0, text.length());
	}
	
	
	/**
	 * DOCTYPE的处理
	 * @since 1.0
	 */
	public void doctypHandler(){
		this.parserCallback.handlerDoctype(getTagContent());
		
		clearTagContent();
	}
	
	/**
	 * 所有标签开始的处理
	 * @since 1.0
	 */
	public void tagStartHandler(){
		this.parserCallback.handlerTagStart(getTagContent());
		
		clearTagContent();
	}
	
	/**
	 * 所有标签结束后的处理
	 * @since 1.0
	 */
	public void tagEndHandler(){
		this.parserCallback.handlerText(getText());//由于后面处理的栈结构，一定要先处理text内容
		this.parserCallback.handlerTagEnd(getTagContent());
		
		clearTagContent();
		clearText();
	}
	
	/**
	 * 简单标签的处理
	 * @since 1.0
	 */
	public void simpleTagHandler(){
		this.parserCallback.handlerSimpleTag(getTagContent());
		
		clearTagContent();
	}
	
	/**
	 * 注释的处理
	 * @since 1.0
	 */
	public void commentHandler(){
		this.parserCallback.handlerComment(getTagContent());
		
		clearTagContent();
	}
	/**
	 * 读取完毕的处理
	 * @since 1.0
	 */
	public void readEndingHandler(){
		this.parserCallback.handlerReadEnding();
		
		clearTagContent();
		clearText();
	}

	
	/**
	 * 把reader流标记到当前位置
	 * @since 1.0
	 */
	public void markReader(){
		if(this.reader.markSupported()){
			try {
				this.reader.mark(currentPosition + 1);
			} catch (IOException e) {
				e.printStackTrace();
				throw new RuntimeException(e);
			}
		}
	}
	
	/**
	 * 把reader流重新定位到刚刚标记的当前位置
	 * @since 1.0
	 */
	public void resetReader(){
		try {
			this.reader.reset();
		} catch (IOException e) {
			e.printStackTrace();
			throw new RuntimeException(e);
		}
	}
}
